﻿<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>GoJS Template Maps -- Northwoods Software</title>
  <!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
  <script src="../release/go.js"></script>
  <script src="../extensions/Figures.js"></script>
  <script src="goIntro.js"></script>
</head>
<body onload="goIntro()">
<div id="container" class="container-fluid">
<div id="content">

<h1>Template Maps</h1>
<p>
Many of the previous examples have provided custom templates for nodes, groups, or links.
Those examples have shown how to make simple adaptations of the templates for particular data instances via data binding.
But what if you want to have nodes with drastically different appearances or behaviors in a
single diagram at the same time?
</p>
<p>
It is possible to define a node template that includes all possible configurations for all of the kinds
of nodes that you want to display.  There would need to be a lot of data binding and/or code to make
the needed changes.  Often you will want to make not-<a>GraphObject.visible</a> large parts of the template in order to make
visible the one panel that you want to show.  But this technique is difficult to use -- templates get
way too complicated too quickly.
</p>
<p>
Instead <b>GoJS</b> supports as many templates as you want -- you choose dynamically which one you want to
use to represent a particular node data.  This does mean potentially a lot of templates, but each one
will be much simpler, easier to write, and easier to maintain.
</p>
<p>
Each <a>Diagram</a> actually holds a <a>Map</a> of templates for each
type of <a>Part</a>: <a>Node</a>, <a>Group</a>, and <a>Link</a>.
Each Map associates a "category" name with a template.
For example, when the diagram wants to create a <a>Node</a> for a particular node data object,
the diagram uses that node data's category to look up the node template in the <a>Diagram.nodeTemplateMap</a>.
Similar lookups are done using the <a>Diagram.groupTemplateMap</a> and the <a>Diagram.linkTemplateMap</a>.
</p>
<p>
Each <a>Diagram</a> initially has its own template maps stocked with predefined categories.
The default category for any data object is the empty string, "".
The <a>Diagram.nodeTemplateMap</a> initially contains for the empty string a very simple <a>Node</a> template
holding a <a>TextBlock</a> whose <a>TextBlock.text</a> property is data bound to the data converted to a string.
You can see the default templates for nodes, groups, and links in a number of the previous examples,
such as the <a href="groups.html#GroupsLinks">Groups and Links</a> example.
</p>
<p>
The value of <a>Diagram.nodeTemplate</a> is just the value of <code>thatDiagram.nodeTemplateMap.get("")</code>.
Setting <a>Diagram.nodeTemplate</a> just replaces the template in <a>Diagram.nodeTemplateMap</a>
named with the empty string.
</p>
<p>
The implementations of all predefined templates are provided in <a href="../extensions/Templates.js">Templates.js</a> in the Extensions directory.
You may wish to copy and adapt these definitions when creating your own templates.
</p>

<h2 id="ExampleOfNodeTemplates">Example of Node templates</h2>
<pre class="lang-js" id="templates">
  // the "simple" template just shows the key string and the color in the background,
  // but it also includes a tooltip that shows the description
  var simpletemplate =
    $(go.Node, "Auto",
      $(go.Shape, "Ellipse",
        new go.Binding("fill", "color")),
      $(go.TextBlock,
        new go.Binding("text", "key")),
      {
        toolTip:
          $("ToolTip",
            $(go.TextBlock, { margin: 4 },
              new go.Binding("text", "desc"))
          )
      }
    );

  // the "detailed" template shows all of the information in a Table Panel
  var detailtemplate =
    $(go.Node, "Auto",
      $(go.Shape, "RoundedRectangle",
        new go.Binding("fill", "color")),
      $(go.Panel, "Table",
        { defaultAlignment: go.Spot.Left },
        $(go.TextBlock, { row: 0, column: 0, columnSpan: 2, font: "bold 12pt sans-serif" },
          new go.Binding("text", "key")),
        $(go.TextBlock, { row: 1, column: 0 }, "Description:"),
        $(go.TextBlock, { row: 1, column: 1 }, new go.Binding("text", "desc")),
        $(go.TextBlock, { row: 2, column: 0 }, "Color:"),
        $(go.TextBlock, { row: 2, column: 1 }, new go.Binding("text", "color"))
      )
    );

  // create the nodeTemplateMap, holding three node templates:
  var templmap = new go.Map(); // In TypeScript you could write: new go.Map&lt;string, go.Node&gt;();
  // for each of the node categories, specify which template to use
  templmap.add("simple", simpletemplate);
  templmap.add("detailed", detailtemplate);
  // for the default category, "", use the same template that Diagrams use by default;
  // this just shows the key value as a simple TextBlock
  templmap.add("", diagram.nodeTemplate);

  diagram.nodeTemplateMap = templmap;

  diagram.model.nodeDataArray = [
    { key: "Alpha", desc: "first letter", color: "green" },  // uses default category: ""
    { key: "Beta", desc: "second letter", color: "lightblue", category: "simple" },
    { key: "Gamma", desc: "third letter", color: "pink", category: "detailed" },
    { key: "Delta", desc: "fourth letter", color: "cyan", category: "detailed" }
  ];
</pre>
<script>goCode("templates", 600, 150)</script>
<p>
If you hover the mouse over the "Beta" node, you will see the tooltip showing the description string.
The detailed template does not bother using tooltips to show extra information because everything is already shown.
</p>
<p>
By default the way that the model and diagram know about the category of a node data or a link data is
by looking at its category property.
If you want to use a different property on the data, for example because you want to use the category
property to mean something different, set <a>Model.nodeCategoryProperty</a> to be the name
of the property that results in the actual category string value.
Or set <a>Model.nodeCategoryProperty</a> to be the empty string to cause all nodes to use the default node template.
</p>

<h2 id="ExampleOfItemTemplates">Example of Item Templates</h2>
<p>
For Panels with a value for <a>Panel.itemArray</a>, there is also the <a>Panel.itemTemplateMap</a>.
As with Nodes and Groups and Links, the <a>Panel.itemTemplate</a> is just a reference to the template named
with the empty string in the <a>Panel.itemTemplateMap</a>.
Similarly, the <a>Panel.itemCategoryProperty</a> names the property on the item data that identifies the
template to use from the itemTemplateMap.
</p>
<pre class="lang-js" id="itemTemplates">
  // create a template map for items
  var itemtemplates = new go.Map(); // In TypeScript you could write: new go.Map&lt;string, go.Panel&gt;();

  // the template when type == "text"
  itemtemplates.add("text",
    $(go.Panel,
      $(go.TextBlock,
        new go.Binding("text"))
    ));

  // the template when type == "button"
  itemtemplates.add("button",
    $("Button",
      $(go.TextBlock,
        new go.Binding("text")),
      // convert a function name into a function value,
      // because functions cannot be represented in JSON format
      new go.Binding("click", "handler",
                      function(name) {
                        if (name === "alert") return raiseAlert;  // defined below
                        return null;
                      })
    ));

  diagram.nodeTemplate =
    $(go.Node, "Vertical",
      $(go.TextBlock,
        new go.Binding("text", "key")),
      $(go.Panel, "Auto",
        $(go.Shape, { fill: "white" }),
        $(go.Panel, "Vertical",
          {
            margin: 3,
            defaultAlignment: go.Spot.Left,
            itemCategoryProperty: "type",  // this property controls the template used
            itemTemplateMap: itemtemplates  // map was defined above
          },
          new go.Binding("itemArray", "info"))
      )
    );

  function raiseAlert(e, obj) {  // here OBJ will be the item Panel
    var node = obj.part;
    alert(node.data.key + ": " + obj.data.text);
  }

  // The model data includes item arrays in the node data.
  diagram.model = new go.GraphLinksModel( [
    { key: "Alpha",
      info: [
              { type: "text", text: "some text" },
              { type: "button", text: "Click me!", handler: "alert"}
            ]
    },
    { key: "Beta",
      info: [
              { type: "text", text: "first line" },
              { type: "button", text: "First Button", handler: "alert"},
              { type: "text", text: "second line" },
              { type: "button", text: "Second Button", handler: "alert" }
            ]
    }
  ],[
    { from: "Alpha", to: "Beta" }
  ]);
</pre>
<script>goCode("itemTemplates", 600, 150)</script>

<h2 id="ExampleOfTableHeaderShowingItemData">Example of Table Header Showing Item Data</h2>
<p>
The natural way to have a distinct header for a Table Panel is to have the first row (i.e. the first item)
hold the data for the header, but have it be styled differently.
In this example we define a "Header" item template in the <a>Panel.itemTemplateMap</a>.
</p>
<pre class="lang-js" id="header">
  var itemTemplateMap = new go.Map();
  itemTemplateMap.add("",
    $(go.Panel, "TableRow",
      $(go.TextBlock, new go.Binding("text", "name"),
        { column: 0, margin: 2, font: "bold 10pt sans-serif" }),
      $(go.TextBlock, new go.Binding("text", "phone"),
        { column: 1, margin: 2 }),
      $(go.TextBlock, new go.Binding("text", "loc"),
        { column: 2, margin: 2 })
    ));
  itemTemplateMap.add("Header",
    $(go.Panel, "TableRow",
      $(go.TextBlock, new go.Binding("text", "name"),
        { column: 0, margin: 2, font: "bold 10pt sans-serif" }),
      $(go.TextBlock, new go.Binding("text", "phone"),
        { column: 1, margin: 2, font: "bold 10pt sans-serif" }),
      $(go.TextBlock, new go.Binding("text", "loc"),
        { column: 2, margin: 2, font: "bold 10pt sans-serif" })
    ));

  diagram.nodeTemplate =
    $(go.Node, "Auto",
      $(go.Shape, { fill: "white" }),
      $(go.Panel, "Table",
        new go.Binding("itemArray", "people"),
        {
          defaultAlignment: go.Spot.Left,
          defaultColumnSeparatorStroke: "black",
          itemTemplateMap: itemTemplateMap
        },
        $(go.RowColumnDefinition,
          { row: 0, background: "lightgray" }),
        $(go.RowColumnDefinition,
          { row: 1, separatorStroke: "black" })
      )
    );

  diagram.model =
    $(go.GraphLinksModel,
      {
        nodeDataArray: [
          { key: "group1",
            people: [
              { name: "Person", phone: "Phone", loc: "Location", category: "Header" },
              { name: "Alice", phone: "2345", loc: "C4-E18" },
              { name: "Bob", phone: "9876", loc: "E1-B34" },
              { name: "Carol", phone: "1111", loc: "C4-E23" },
              { name: "Ted", phone: "2222", loc: "C4-E197" },
              { name: "Robert", phone: "5656", loc: "B1-A27" },
              { name: "Natalie", phone: "5698", loc: "B1-B6" }
            ] }
        ],
        linkDataArray: [
        ]
      }
    );
</pre>
<script>goCode("header", 600, 200)</script>
<p>
If you do not want to have the header data in the itemArray,
and you want to define the header in the node template rather than as an item template,
see the example in <a href="itemArrays.html">Item Arrays</a>.
</p>

<h2 id="ChangingCategoryOfPart">Changing category of a Part</h2>
<p>
To change the representation of a data object, call <a>Model.setCategoryForNodeData</a>
or <a>GraphLinksModel.setCategoryForLinkData</a>.
(If you set the <a>Part.category</a> of a data bound <a>Part</a>, it will call the Model method for you.)
This causes the diagram to discard any existing Part for the data and re-create it using the new template that
is associated with the new category value.
</p>
<pre class="lang-js" id="changingCategory">
  // this function changes the category of the node data to cause the Node to be replaced
  function changeCategory(e, obj) {
    var node = obj.part;
    if (node) {
      var diagram = node.diagram;
      diagram.startTransaction("changeCategory");
      var cat = diagram.model.getCategoryForNodeData(node.data);
      if (cat === "simple")
        cat = "detailed";
      else
        cat = "simple";
      diagram.model.setCategoryForNodeData(node.data, cat);
      diagram.commitTransaction("changeCategory");
    }
  }

  // The "simple" template just shows the key string and the color in the background.
  // There is a Button to invoke the changeCategory function.
  var simpletemplate =
    $(go.Node, "Spot",
      $(go.Panel, "Auto",
        $(go.Shape, "Ellipse",
          new go.Binding("fill", "color")),
        $(go.TextBlock,
          new go.Binding("text", "key"))
      ),
      $("Button",
        { alignment: go.Spot.TopRight },
        $(go.Shape, "AsteriskLine", { width: 8, height: 8 }),
        { click: changeCategory })
    );

  // The "detailed" template shows all of the information in a Table Panel.
  // There is a Button to invoke the changeCategory function.
  var detailtemplate =
    $(go.Node, "Spot",
      $(go.Panel, "Auto",
        $(go.Shape, "RoundedRectangle",
          new go.Binding("fill", "color")),
        $(go.Panel, "Table",
          { defaultAlignment: go.Spot.Left },
          $(go.TextBlock, { row: 0, column: 0, columnSpan: 2, font: "bold 12pt sans-serif" },
            new go.Binding("text", "key")),
          $(go.TextBlock, { row: 1, column: 0 }, "Description:"),
          $(go.TextBlock, { row: 1, column: 1 }, new go.Binding("text", "desc")),
          $(go.TextBlock, { row: 2, column: 0 }, "Color:"),
          $(go.TextBlock, { row: 2, column: 1 }, new go.Binding("text", "color"))
        )
      ),
      $("Button",
        { alignment: go.Spot.TopRight },
        $(go.Shape, "AsteriskLine", { width: 8, height: 8 }),
        { click: changeCategory })
    );

  var templmap = new go.Map(); // In TypeScript you could write: new go.Map&lt;string, go.Node&gt;();
  templmap.add("simple", simpletemplate);
  templmap.add("detailed", detailtemplate);
  diagram.nodeTemplateMap = templmap;

  diagram.layout = $(go.TreeLayout);

  diagram.model.nodeDataArray = [
    { key: "Beta", desc: "second letter", color: "lightblue", category: "simple" },
    { key: "Gamma", desc: "third letter", color: "pink", category: "detailed" },
    { key: "Delta", desc: "fourth letter", color: "cyan", category: "detailed" }
  ];
  diagram.model.linkDataArray = [
    { from: "Beta", to: "Gamma" },
    { from: "Gamma", to: "Delta" }
  ];
</pre>
<script>goCode("changingCategory", 600, 150)</script>
<p>
Click on the "asterisk" button on any node to toggle dynamically between the "simple" and the "detailed" category for each node.
</p>

<h2 id="ChangingTemplateMaps">Changing template maps</h2>
<p>
You can also replace one or all of the diagram's template maps (e.g. <a>Diagram.nodeTemplateMap</a>)
in order to discard and re-create all of the nodes in the diagram.
If you are only using the default template for nodes, you would only need to replace the <a>Diagram.nodeTemplate</a>.
</p>
<p>
One common circumstance for doing this is as the <a>Diagram.scale</a> changes.
When the user zooms out far enough, there is no point in having too much detail about each of the nodes.
</p>
<p>
If you zoom out in this example, the <a>DiagramEvent</a> listener will detect when the <a>Diagram.scale</a>
becomes small enough to use the simpler template for all of the nodes.
Zoom in again and suddenly it uses the more detailed template.
</p>
<pre class="lang-js" id="changingMap">
  // The "simple" template just shows the key string and the color in the background.
  var simpletemplate =
    $(go.Node, "Spot",
      $(go.Panel, "Auto",
        $(go.Shape, "Ellipse",
          new go.Binding("fill", "color")),
        $(go.TextBlock,
          new go.Binding("text", "key"))
      )
    );

  // The "detailed" template shows all of the information in a Table Panel.
  var detailtemplate =
    $(go.Node, "Spot",
      $(go.Panel, "Auto",
        $(go.Shape, "RoundedRectangle",
          new go.Binding("fill", "color")),
        $(go.Panel, "Table",
          { defaultAlignment: go.Spot.Left },
          $(go.TextBlock, { row: 0, column: 0, columnSpan: 2, font: "bold 12pt sans-serif" },
            new go.Binding("text", "key")),
          $(go.TextBlock, { row: 1, column: 0 }, "Description:"),
          $(go.TextBlock, { row: 1, column: 1 }, new go.Binding("text", "desc")),
          $(go.TextBlock, { row: 2, column: 0 }, "Color:"),
          $(go.TextBlock, { row: 2, column: 1 }, new go.Binding("text", "color"))
        )
      )
    );

  diagram.layout = $(go.TreeLayout);

  diagram.model.nodeDataArray = [
    { key: "Beta", desc: "second letter", color: "lightblue" },
    { key: "Gamma", desc: "third letter", color: "pink" },
    { key: "Delta", desc: "fourth letter", color: "cyan" }
  ];
  diagram.model.linkDataArray = [
    { from: "Beta", to: "Gamma" },
    { from: "Gamma", to: "Delta" }
  ];

  // initially use the detailed templates
  diagram.nodeTemplate = detailtemplate;

  diagram.addDiagramListener("ViewportBoundsChanged",
    function (e) {
      if (diagram.scale &lt; 0.9) {
        diagram.nodeTemplate = simpletemplate;
      } else {
        diagram.nodeTemplate = detailtemplate;
      }
    });

  myDiagram = diagram;  // make accessible to the HTML buttons
</pre>
<script>goCode("changingMap", 600, 150)</script>
<input id="ZoomOut" type="button" onclick="myDiagram.commandHandler.decreaseZoom()" value="Zoom Out" />
<input id="ZoomIn" type="button" onclick="myDiagram.commandHandler.increaseZoom()" value="Zoom In" />
<p>
Caution: if you modify a template <a>Map</a>, there is no notification that the map has changed.
You will need to call <a>Diagram.rebuildParts</a> explicitly.
If you are replacing the <a>Diagram.nodeTemplate</a> or the <a>Diagram.nodeTemplateMap</a>
or the corresponding properties for Groups or Links, the Diagram property setters will automatically
call <a>Diagram.rebuildParts</a>.
</p>
<p>
When one or more templates are replaced in a diagram, layouts are automatically performed again.
</p>

</div>
</div>
</body>
</html>
